/* 
 * Copyright (C) 2003 Heiko Noordhof <heiko.noordhof@xs4all.nl>
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * A copy of the GNU General Public License (GPL) is found in the
 * COPYING file that is distributed with this program.
 */


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>  
#include <sys/ioctl.h>
#include <linux/serial.h>

#include "serctrl.h"


static int fd;
static struct termios oldterminfo;


int checkfilename(const char *filename)
{
     if (filename == NULL) {
	  fprintf(stderr, "ERROR: NULL pointer filename.\n");
	  return 0;
     }
     if (strlen(filename) > PATH_MAX) {
	  fprintf(stderr, "ERROR: filename too long.\n");
	  return 0;
     }
     return 1;
}


int ischaracterdevice(const char *filename)
{
     struct stat status;
     mode_t mode;

     if (stat(filename, &status) == -1) {
	  perror("ischaracterdevice()");
	  return 0;
     };
     mode = status.st_mode;
     if (!S_ISCHR(mode)) {
	  fprintf(stderr, "ERROR: %s is not a character device.\n", filename);
	  return 0;
     }
     return 1;
}     


int isserialdevice(const char *devicename)
{
     struct serial_struct serinfo;

     if (!isatty(fd)) {
	  fprintf(stderr, "ERROR: %s is not a tty device.\n", devicename);
	  return 0;
     }
     serinfo.reserved_char[0] = 0;
     if (ioctl(fd, TIOCGSERIAL, &serinfo) < 0) {
	  fprintf(stderr, "ERROR: %s is not a serial device.\n", devicename);
	  return 0;
     }
     return 1;    
}


void erroropenserial(char *devicename)
{
     fprintf(stderr, "Could not open device file %s.\n", devicename);
     exit(EXIT_FAILURE);
}


void openserial(char *devicename)
{
     struct termios attr;

     if (!ischaracterdevice(devicename)) {
	  erroropenserial(devicename);
     }
     if ((fd = open(devicename, O_RDWR)) == -1) { 
	  perror("openserial(): open()");
	  erroropenserial(devicename);
     }
     if (!isserialdevice(devicename)) {
	  erroropenserial(devicename);
     }
     if (tcgetattr(fd, &oldterminfo) == -1) {
	  perror("openserial()");
	  closeserial();
	  erroropenserial(devicename);
     }
     attr = oldterminfo;
     attr.c_cflag |= CRTSCTS | CLOCAL;
     attr.c_oflag = 0;
     if (tcflush(fd, TCIOFLUSH) == -1) {
	  perror("openserial(): tcflush()");
	  closeserial();
	  erroropenserial(devicename);
     }
     if (tcsetattr(fd, TCSANOW, &attr) == -1) {
	  perror("initserial(): tcsetattr()");
	  closeserial();
	  erroropenserial(devicename);
     }
     setRTS(0);
     setDTR(1);
}


int closeserial(void)
{
     tcsetattr(fd, TCSANOW, &oldterminfo);
     if (close(fd) == -1) {
	  perror("closeserial()");
	  return 0;
     }
     return 1;
}


int setRTS(unsigned short level)
{
     int status;

     if (ioctl(fd, TIOCMGET, &status) == -1) {
	  perror("setRTS()");
	  return 0;
     }
     if (level) status |= TIOCM_RTS;
     else status &= ~TIOCM_RTS;
     if (ioctl(fd, TIOCMSET, &status) == -1) {
	  perror("setRTS()");
	  return 0;
     }
     return 1;
}


int setDTR(unsigned short level)
{
     int status;

     if (ioctl(fd, TIOCMGET, &status) == -1) {
	  perror("setDTR()");
	  return 0;
     }
     if (level) status |= TIOCM_DTR;
     else status &= ~TIOCM_DTR;
     if (ioctl(fd, TIOCMSET, &status) == -1) {
	  perror("setDTR");
	  return 0;
     }
     return 1;
}


switx waitswitch(void)
{
     int status, mask;
     unsigned switches;
     switx sw;
     static unsigned oldswitches;

     mask = TIOCM_CAR | TIOCM_DSR | TIOCM_CTS | TIOCM_RNG;

     /* The ioctl() call below does not return until one of
      * the switches changes state.
      */
     if (ioctl(fd, TIOCMIWAIT, mask) == -1) {
	  perror("waitswitch(): TIOCMIWAIT");
	  closeserial();
	  exit(EXIT_FAILURE);
     }

     /* ...so, when we arrive here, there *must* be one of switches
      * pressed, or released.  However, due to bouncing of the switch
      * we may miss it when we retrieve the status at an unlucky
      * moment (this does happen..).  In that case we do not know
      * which button changed state.
      * 
      * In order not to have to return "button #0", below is a (quite
      * busy) loop that doesn't end until a button is found that
      * changed state.
      *
      * FIXME: Is it really OK to assume we will actually find a
      * switch that changed state?  If not, we could be spinning here
      * until the user switches some switch again.  To minimize the
      * impact in that case, there's a usleep(), but what delay time
      * to choose?  Or are we even better off without a usleep() in
      * order to have a better chance to catch the switch?
      */

     do {
	  if (ioctl(fd, TIOCMGET, &status) == -1) {
	       perror("waitswitch(): TIOCMGET");
	       closeserial();
	       exit(EXIT_FAILURE);
	  }

	  switches = status & mask;

	  switch (switches ^ oldswitches) {
	  case TIOCM_CAR:
	       sw.nr = 1;
	       sw.level = switches & TIOCM_CAR ? on : off;
	       break;
	  case TIOCM_DSR:
	       sw.nr = 2;
	       sw.level = switches & TIOCM_DSR ? on : off;
	       break;
	  case TIOCM_CTS:
	       sw.nr = 3;
	       sw.level = switches & TIOCM_CTS ? on : off;
	       break;
	  case TIOCM_RNG:
	       sw.nr = 4;
	       sw.level = switches & TIOCM_RNG ? on : off;
	       break;
	  default:
	       sw.nr = 0;
	       sw.level = 0;
	  }
	  usleep(1000);
     } while (sw.nr == 0);

     oldswitches = switches;
 
     return sw;
}
